package com.hqd.ch03.v49.boot.autoconfigure.condition;

import com.hqd.ch03.v49.context.annotation.Condition;
import com.hqd.ch03.v49.context.annotation.ConditionContext;
import com.hqd.ch03.v49.core.type.AnnotatedTypeMetadata;
import com.hqd.ch03.v49.core.type.AnnotationMetadata;
import com.hqd.ch03.v49.core.type.ClassMetadata;
import com.hqd.ch03.v49.core.type.MethodMetadata;

public abstract class SpringBootCondition implements Condition {

    private static String getClassOrMethodName(AnnotatedTypeMetadata metadata) {
        if (metadata instanceof ClassMetadata) {
            ClassMetadata classMetadata = (ClassMetadata) metadata;
            return classMetadata.getClassName();
        }
        MethodMetadata methodMetadata = (MethodMetadata) metadata;
        return methodMetadata.getDeclaringClassName() + "#" + methodMetadata.getMethodName();
    }

    @Override
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();
        } catch (NoClassDefFoundError ex) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
                    + ex.getMessage() + " not found. Make sure your own configuration does not rely on "
                    + "that class. This can also happen if you are "
                    + "@ComponentScanning a springframework package (e.g. if you "
                    + "put a @ComponentScan in the default package by mistake)", ex);
        } catch (RuntimeException ex) {
            throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
        }
    }

    private String getName(AnnotatedTypeMetadata metadata) {
        if (metadata instanceof AnnotationMetadata) {
            return ((AnnotationMetadata) metadata).getClassName();
        }
        if (metadata instanceof MethodMetadata) {
            MethodMetadata methodMetadata = (MethodMetadata) metadata;
            return methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName();
        }
        return metadata.toString();
    }


    private void recordEvaluation(ConditionContext context, String classOrMethodName, ConditionOutcome outcome) {
        if (context.getBeanFactory() != null) {
            ConditionEvaluationReport.get(context.getBeanFactory()).recordConditionEvaluation(classOrMethodName, this,
                    outcome);
        }
    }

    /**
     * Determine the outcome of the match along with suitable log output.
     *
     * @param context  the condition context
     * @param metadata the annotation metadata
     * @return the condition outcome
     */
    public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);

    /**
     * Return true if any of the specified conditions match.
     *
     * @param context    the context
     * @param metadata   the annotation meta-data
     * @param conditions conditions to test
     * @return {@code true} if any condition matches.
     */
    protected final boolean anyMatches(ConditionContext context, AnnotatedTypeMetadata metadata,
                                       Condition... conditions) {
        for (Condition condition : conditions) {
            if (matches(context, metadata, condition)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return true if any of the specified condition matches.
     *
     * @param context   the context
     * @param metadata  the annotation meta-data
     * @param condition condition to test
     * @return {@code true} if the condition matches.
     */
    protected final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata, Condition condition) {
        if (condition instanceof SpringBootCondition) {
            return ((SpringBootCondition) condition).getMatchOutcome(context, metadata).isMatch();
        }
        return condition.matches(context, metadata);
    }

}
